import fs from "fs-extra";
import path from "path";
import { ClassDeclaration, ClassStaticPropertyTypes, GetAccessorDeclaration, Identifier, InterfaceDeclaration, MethodDeclaration, MethodSignature, ModuleDeclaration, NamedNodeSpecificBase, Node, Project, PropertyDeclaration, PropertySignature, ReferenceFindableNode, SetAccessorDeclaration, SyntaxKind } from "ts-morph";

interface MemberInfo {
    id: number
    type: SyntaxKind;
    name: string;
    parameterTypes?: string[]
}

interface ReferTest {
    interfaceRefered: boolean;
    usedMap: Record<number, number>;
    members: MemberInfo[];
}

interface Filtered {
    class: string;
    member: MemberInfo;
}

export class FindUsage {
    private readonly ReservedMethods = [
        "UnityEngine.RectOffset~left",
        "UnityEngine.RectOffset~right",
        "UnityEngine.RectOffset~top",
        "UnityEngine.RectOffset~bottom",
        "UnityEngine.RectOffset~horizontal",
        "UnityEngine.RectOffset~vertical",
        "UnityEngine.Screen~currentResolution",
        "Game.MemValueRegister~GetString",
        "Game.MemValueRegister~GetBool",
    ];
    private readonly __keep_incompatibility = '[__keep_incompatibility]';

    private nextId = 1;

    public async startFind(projRoot: string, fyConvertorRoot: string): Promise<void> {
        const project = new Project({
            tsConfigFilePath: path.join(projRoot, 'TsScripts/tsconfig.json')
        });
        // index.d.ts
        const csharpDTS = path.join(projRoot, 'Assets/Gen/Typing/csharp/index.d.ts');
        const csharpSrc = project.getSourceFile(csharpDTS)!;
        const CS = csharpSrc.getModule('CS')!;
        const csharpOut = await this.findInModule(CS, projRoot, [csharpDTS], true);
        await this.writeToWrapConfig(projRoot, csharpOut.uselessClasses);
        await this.writeToPuertsFilter(projRoot, csharpOut.uselessMembers);
    }

    private findInModule(moduleDcl: ModuleDeclaration, projRoot: string, excludes: string[], checkWrapped: boolean): { uselessClasses: string[], uselessMembers: Filtered[] } {
        const uselessClasses: string[] = [];
        const uselessMembers: Filtered[] = [];
        this.findInModuleInternal(moduleDcl, projRoot, excludes, checkWrapped, uselessClasses, uselessMembers);
        return { uselessClasses, uselessMembers };
    }

    private findInModuleInternal(moduleDcl: ModuleDeclaration, projRoot: string, excludes: string[], checkWrapped: boolean, uselessClasses: string[], uselessMembers: Filtered[]): void {
        const modules = moduleDcl.getModules();
        for (const m of modules) {
            this.findInModuleInternal(m, projRoot, excludes, checkWrapped, uselessClasses, uselessMembers);    
        }
        // interfaces
        for (const itf of moduleDcl.getInterfaces()) {
            const itfName = (moduleDcl.getName() + '.' + itf.getName()).replace('CS.', '');
            if (checkWrapped && !this.isRealWrapped(itfName, projRoot)) continue;

            const rt = this.isClassReferedOutside(itf, excludes);
            let anyUsed = false;
            for (const member of rt.members) {
                if (rt.usedMap[member.id] == 0 && !this.ReservedMethods.includes(itfName + '~' + member.name)) {
                    uselessMembers.push({ class: itfName, member });
                } else {
                    anyUsed = true;
                }
            }
            if (!anyUsed && !rt.interfaceRefered) {
                uselessClasses.push(itfName);
            }
        }
        // classes
        for (const cls of moduleDcl.getClasses()) {
            const clsName = (moduleDcl.getName() + '.' + cls.getName()).replace('CS.', '');
            if (checkWrapped && !this.isRealWrapped(clsName, projRoot)) continue;

            // if (clsName != 'UnityEngine.Material') continue;
            const rt = this.isClassReferedOutside(cls, excludes);
            let anyUsed = false;
            for (const member of rt.members) {
                if (rt.usedMap[member.id] == 0 && !this.ReservedMethods.includes(clsName + '~' + member.name)) {
                    uselessMembers.push({ class: clsName, member });
                } else {
                    anyUsed = true;
                }
            }
            if (!anyUsed && !rt.interfaceRefered) {
                uselessClasses.push(clsName);
            }
        }
    }

    private async writeToPuertsFilter(projRoot: string, uselessMembers: Filtered[]): Promise<void> {
        const puertsFilter = path.join(projRoot, 'Assets/Editor/Puerts/PuertsFilter.cs');
        const content = await fs.readFile(puertsFilter, 'utf-8');
        const replaced = '// AUTO FILTER, DONOT DELETE THIS LINE!';
        if (!content.includes(replaced)) {
            console.error('you should put the following code in PuertsFilter.cs first.');
            return;
        }
        const addFiltered: string[] = [];
        for (const f of uselessMembers) {
            let str: string;
            if (f.member.parameterTypes?.length) {
                str = `new List<string>(){"${f.class}", "${f.member.name}", "*"},`;
            } else {
                str = `new List<string>(){"${f.class}", "${f.member.name}"},`;
            }
            // todo 收集puerts里的现有规则，生成带参数的
            if (content.search(str) < 0 && !addFiltered.includes(str)) {
                addFiltered.push(str);
            } else {
                console.log('already filtered:', f.class, f.member);
            }
        }
        const newContent = content.replace(replaced, [replaced].concat(addFiltered).join('\n                '));
        await fs.writeFile(puertsFilter, newContent, 'utf-8');
    }

    private async writeToWrapConfig(projRoot: string, uselessClasses: string[]): Promise<void> {
        const ucs = uselessClasses.map((v) => {
            const arr = v.split('.');
            return arr[arr.length - 1];
        });
        const wrapConfig = path.join(projRoot, 'Assets/Editor/Puerts/WrapConfig.cs');
        const content = await fs.readFile(wrapConfig, 'utf-8');
        const lines = content.split(/\r?\n/);
        for (let i = 0, len = lines.length; i < len; i++) {
            const line = lines[i];
            const mch = line.match(/typeof\((.+)\)/);
            if (mch) {
                const names = mch[1].split('.');
                if (ucs.includes(names[names.length - 1])) {
                    lines[i] = line.replace('typeof', '// typeof');
                }
            }
        }
        const newContent = lines.join('\n');
        if (newContent != content) {
            await fs.writeFile(wrapConfig, newContent, 'utf-8');
        }
    }

    private isRealWrapped(name: string, projRoot: string): boolean {
        const wrapName = path.join(projRoot, 'Assets/Gen', name.replace('CS.', '').replaceAll('.', '_') + '_Wrap.cs');
        return fs.existsSync(wrapName);
    }

    private isClassReferedOutside(cls: InterfaceDeclaration | ClassDeclaration, excludes: string[]): ReferTest {
        const interfaceRefered = this.isNodeReferedOutside(cls, excludes);
        const usedMap: { [name: string]: number } = {};
        const members: MemberInfo[] = [];
        const test = { interfaceRefered, usedMap, members };

        const handler = (value: PropertySignature | PropertyDeclaration | MethodSignature | MethodDeclaration | GetAccessorDeclaration | SetAccessorDeclaration | ClassStaticPropertyTypes) => {
            const name = value.getName();
            if (name === this.__keep_incompatibility) return;

            const info: MemberInfo = {
                id: this.nextId,
                type: value.getKind(),
                name
            };
            if (value.isKind(SyntaxKind.MethodDeclaration)) {
                const params = value.getParameters();
                const paramTypes = params.map((v) => v.getType().getText());
                info.parameterTypes = paramTypes;
            }
            test.members.push(info);

            if (!(name in test.usedMap)) test.usedMap[this.nextId] = 0;
            if (this.isNodeReferedOutside(value, excludes)) test.usedMap[this.nextId] += 1;
            this.nextId++;
        }

        cls.getProperties().forEach(handler);
        cls.getMethods().forEach(handler);

        if (cls.isKind(SyntaxKind.ClassDeclaration)) {
            cls.getGetAccessors().forEach(handler);
            cls.getSetAccessors().forEach(handler);
            cls.getStaticProperties().forEach(handler);
            cls.getStaticMethods().forEach(handler);
        }

        return test;
    }

    private isNodeReferedOutside(node: ReferenceFindableNode, excludes: string[]): boolean {
        // setter和getter的findReferencesAsNodes会互相包含对方
        // const refs = node.findReferencesAsNodes();
        // if (refs.length == 0) {
        //     return false;
        // }

        // return true;
        const refSymbols = node.findReferences();
        let used = false;
        for (const refSymbol of refSymbols) {
            for (const ref of refSymbol.getReferences()) {
                // console.log(`${refSymbol.getDefinition().getName()} refered in ${ref.getSourceFile().getFilePath()}`);
                if (!excludes.includes(path.resolve(ref.getSourceFile().getFilePath()))) {
                    const n = ref.getNode();
                    const kn = n.getKindName();
                    used = true;
                    break;
                }
            }
            if (used) break;
        }
        return used;
    }
};
